package com.gitee.ixtf.vertx.guice

import cn.hutool.core.convert.ConverterRegistry
import com.gitee.ixtf.vertx.ParameterConverterProvider
import com.gitee.ixtf.vertx.internal.RestfulMethod
import com.google.inject.AbstractModule
import com.google.inject.Key
import com.google.inject.Provides
import com.google.inject.multibindings.OptionalBinder.newOptionalBinder
import com.google.inject.util.Types
import com.sun.security.auth.UserPrincipal
import graphql.GraphQL
import io.netty.handler.codec.http.HttpHeaderNames.*
import io.vertx.core.Future
import io.vertx.core.Vertx
import io.vertx.core.eventbus.EventBus
import io.vertx.core.file.FileSystem
import io.vertx.core.http.HttpMethod
import io.vertx.ext.auth.User
import io.vertx.ext.web.RoutingContext
import io.vertx.ext.web.client.WebClient
import io.vertx.ext.web.handler.AuthenticationHandler
import io.vertx.ext.web.handler.BodyHandler
import io.vertx.ext.web.handler.CorsHandler
import jakarta.inject.Singleton
import java.security.Principal
import org.dataloader.DataLoaderRegistry

open class VertxGuice(private val vertx: Vertx) : AbstractModule() {
  init {
    ConverterRegistry.getInstance().apply {
      putCustom(Int::class.java) { value, defaultValue ->
        (value ?: defaultValue).toString().toInt()
      }
      putCustom(Long::class.java) { value, defaultValue ->
        (value ?: defaultValue).toString().toLong()
      }
      putCustom(Float::class.java) { value, defaultValue ->
        (value ?: defaultValue).toString().toFloat()
      }
      putCustom(Double::class.java) { value, defaultValue ->
        (value ?: defaultValue).toString().toDouble()
      }
      putCustom(Principal::class.java) { value, _ ->
        when (value) {
          is RoutingContext -> principal(value)
          is User -> principal(value)
          else -> throw RuntimeException("Principal")
        }
      }
    }
  }

  override fun configure() {
    bind(Vertx::class.java).toInstance(vertx)
    bind(EventBus::class.java).toInstance(vertx.eventBus())
    bind(FileSystem::class.java).toInstance(vertx.fileSystem())
    bind(WebClient::class.java).toInstance(WebClient.create(vertx))
    newOptionalBinder(binder(), GraphQL::class.java)
    newOptionalBinder(binder(), DataLoaderRegistry::class.java)
    newOptionalBinder(binder(), Key.get(Types.collectionOf(RestfulMethod::class.java)))
    newOptionalBinder(binder(), object : Key<Future<AuthenticationHandler>>() {})
    newOptionalBinder(binder(), Key.get(Types.collectionOf(ParameterConverterProvider::class.java)))
  }

  protected open fun principal(rc: RoutingContext): Principal =
      rc.request()
          .getHeader("x-api-principal")
          ?.takeIf { it.isNotBlank() }
          ?.let { UserPrincipal(it) } ?: principal(rc.user())

  protected open fun principal(user: User): Principal =
      user.run {
        val sub = principal().getString("sub") ?: attributes().getString("sub")
        UserPrincipal(sub)
      }

  @Singleton
  @Provides
  private fun CorsHandler(): CorsHandler =
      CorsHandler.create().apply {
        allowedMethod(HttpMethod.OPTIONS)
        allowedMethod(HttpMethod.POST)
        allowedMethod(HttpMethod.PUT)
        allowedMethod(HttpMethod.GET)
        allowedMethod(HttpMethod.DELETE)

        allowedHeader("x-api-principal")
        allowedHeader("x-api-service")
        allowedHeader("x-api-instance")
        allowedHeader("x-api-action")
        allowedHeader(ACCESS_CONTROL_REQUEST_METHOD.toString())
        allowedHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS.toString())
        allowedHeader(ACCESS_CONTROL_ALLOW_ORIGIN.toString())
        allowedHeader(ACCESS_CONTROL_ALLOW_HEADERS.toString())
        allowedHeader(AUTHORIZATION.toString())
        allowedHeader(CACHE_CONTROL.toString())
        allowedHeader(ACCEPT.toString())
        allowedHeader(ORIGIN.toString())
        allowedHeader(CONTENT_TYPE.toString())

        allowCredentials(true)
        build(this)
      }

  protected open fun build(handler: CorsHandler) {
    handler.addRelativeOrigin(".*.")
  }

  @Singleton
  @Provides
  private fun BodyHandler(): BodyHandler =
      BodyHandler.create().apply {
        setDeleteUploadedFilesOnEnd(true)
        build(this)
      }

  protected open fun build(handler: BodyHandler) = Unit
}
